前端程式設計筆記 (2026.4)

HTML / CSS / JS / AJAX / Python Flask 職訓課程筆記彙整&快速工具

GARY-KAO

第十三章:CRUD 系統實戰整合

1. CRUD 流程解析

這套系統展示了完整的前端 CRUD (Create, Read, Update, Delete) 核心架構。透過 jQuery 的 AJAX 搭配 Bootstrap UI 與 SweetAlert2,完成了一套不需重新整理網頁的非同步資料處理流程。

步驟一:C - Create (新增資料)

對應檔案: 20260310-CRUD-index.html
新增資料的流程包含「表單驗證」與「資料拋送」兩個階段:

  • 防呆與重複驗證 (GET):
    當使用者輸入完「品名」並移開游標 (blur 事件) 時,程式會先發送一個 GET 請求到 http://localhost:3000/product?pname=...。這是為了檢查資料庫中是否已經存在相同的產品名稱。如果陣列長度大於 0,代表名稱重複,會立刻將輸入框標示為紅色錯誤 (invalid) 並阻擋後續送出。
  • 打包 JSON 資料:
    當使用者點擊「確認」且所有旗標 (flag) 皆為 true 時,會將表單內的值 (username, pname, pnum, size) 與當下時間 (createdAt) 打包成一個 JavaScript 物件。
  • 發送新增請求 (POST):
    使用 AJAX 的 POST 方法發送到後端 API。
    • 必須設定 contentType: "application/json"
    • 使用 JSON.stringify(jsonData) 將物件轉為純文字字串送出。
    • 成功後,呼叫 SweetAlert2 顯示成功動畫,並利用 .val("").blur() 清空表單以利下一筆輸入。

步驟二:R - Read (讀取與渲染列表)

對應檔案: 20260310-CRUD-showTable.html (前半部)
進入產品列表頁面時,第一件事就是把資料庫的資料抓出來顯示:

  • 發送讀取請求 (GET):
    $(function() {...}) (網頁載入完成) 時,立刻發送 AJAX GET 請求到 http://localhost:3000/product 取得所有產品的 JSON 陣列。
  • 動態渲染表格 (Render Table):
    呼叫自訂函數 renderTable(data),使用 forEach 迴圈遍歷所有資料。利用 ES6 樣板字面值 (反引號) 拼接出 <tr><td> 的 HTML 結構,最後用 $("#mybody").append(strHTML) 將其塞入網頁表格中。
🎯 關鍵技巧:埋設資料屬性 (Data Attributes)
在渲染「更新」與「刪除」按鈕時,程式碼大量使用了 HTML5 的 data-* 屬性(例如 data-id="${item.id}"data-pname="${item.pname}")。這非常重要,這等於是把資料「綁」在按鈕上,讓後續點擊時能精準知道要操作哪一筆資料。

步驟三:U - Update (更新資料)

對應檔案: 20260310-CRUD-showTable.html (更新功能)
更新流程分為「帶入舊資料」與「送出新資料」:

🎯 關鍵技巧:事件委派監聽 (Event Delegation)
因為表格的按鈕是 AJAX 非同步「事後」才長出來的,如果直接用 $(".updateBtn").click(...) 會監聽不到。必須使用 $(document).on("click", ".updateBtn", function() {...}) 將監聽綁定在父層文件上。
  • 資料回填至 Modal:
    點擊「更新」按鈕時,會透過 $(this).data("id") 等語法,將剛剛埋在按鈕上的舊資料抓出來,填入到 Bootstrap Modal 彈出視窗的對應輸入框中,並將唯一的 id 存入全域變數 uid 中備用。
  • 發送更新請求 (PATCH):
    在 Modal 中修改完數量或尺寸並點擊確認後,會觸發 AJAX 請求。
    • HTTP 方法: 使用 PATCH (用於部分更新) 或 PUT (用於完全覆蓋)。這裡使用 PATCH
    • URL 路由: 網址必須加上特定的 ID,例如 .../product/${uid},精準告訴後端要修改哪一筆。
    • 成功後,使用 location.href 重新載入頁面以顯示最新資料。

步驟四:D - Delete (刪除資料)

對應檔案: 20260310-CRUD-showTable.html (刪除功能)
刪除是最具破壞性的動作,因此必須加上防護機制:

  • 確認視窗:
    點擊刪除按鈕時(同樣使用事件委派 $(document).on(...) 監聽),會先彈出 SweetAlert2 詢問「確認刪除?」。
  • 取得唯一識別碼:
    如果使用者點擊「確認」,則透過 let delete_id = $(this).data("id"); 抓出要刪除的資料 ID。
  • 發送刪除請求 (DELETE):
    發送 AJAX DELETE 請求至 http://localhost:3000/product/${delete_id}
    • DELETE 請求通常不需要傳遞 data 本體,只要 URL 中的 ID 正確即可。
    • 伺服器刪除成功後,前端執行網頁重整,完成資料畫面的同步。

2.新增資料 (Create)

現在我們正式進入 CRUD 的實作階段。第一步是建立讓使用者輸入資料的介面 (UI)。我們不使用傳統會造成網頁重整的 <form>,而是利用 Bootstrap 的卡片 (Card) 與獨立的輸入框元件來刻劃版面,準備搭配 AJAX 使用。

2-1. 準備 index 檔案、刻入架構

這段 HTML 程式碼負責建構「新增產品」的表單外觀,以及預先埋設「驗證提示」的區塊:

<div class="card shadow-lg">
    <div class="card-header text-bg-danger fw-900 h3 text-center">新增產品</div>
    <div class="card-body">
        
        <!-- 1. 訂購者欄位 -->
        <div class="mb-3">
            <label for="username" class="form-label">訂購者名稱</label>
            <input type="text" class="form-control" placeholder="字數1~8" id="username" name="username">
            <div class="valid-feedback">符合規定</div>
            <div class="invalid-feedback">不符合規定</div>
        </div>

        <!-- 2. 品名欄位 (具備專屬動態回饋 ID) -->
        <div class="mb-3">
            <label for="" class="form-label">品名</label>
            <input type="text" class="form-control" placeholder="字數1~8" id="pname">
            <div class="valid-feedback" id="pname_valid-feedback">符合規定</div>
            <div class="invalid-feedback" id="pname_invalid-feedback">不符合規定</div>
        </div>

        <!-- 3. 數量欄位 -->
        <div class="mb-3">
            <label for="" class="form-label">數量(數量1~10)</label>
            <input type="number" min="1" max="10" value="1" class="form-control" id="pnum">
            <div class="valid-feedback">符合規定</div>
            <div class="invalid-feedback">不符合規定</div>
        </div>

        <!-- 4. 尺寸選單 -->
        <div class="mb-3">
            <label for="" class="form-label">尺寸</label>
            <select name="" id="size" class="form-select form-select-lg">
                <option value="大">大</option>
                <option value="中">中</option>
                <option value="小">小</option>
            </select>
        </div>

        <!-- 5. 按鈕區 -->
        <div class="text-center my-4">
            <button class="btn btn-outline-secondary">取消</button>
            <button class="btn btn-primary" id="ok_btn">確認</button>
        </div>

    </div>
</div>

核心設計思維解析:

  • UI 視覺包裝 (Card Component): 使用 .card 搭配 .shadow-lg (大陰影) 將表單包裝成一個獨立的浮動面板,提升整體畫面的層次感與專業度。
  • ID 命名綁定 (The Hooks): 每個 <input><select> 以及最後的確認按鈕 (#ok_btn) 都給予了明確的 id。這些 ID 是 jQuery 後續用來「抓取使用者輸入值」和「綁定點擊事件」的唯一鉤子 (Hooks)。
  • 預留驗證回饋區塊 (Validation Feedback): 這是此架構最關鍵的防呆設計。在每個輸入框下方,都預先寫好了 .valid-feedback (綠字) 與 .invalid-feedback (紅字) 的 div 區塊。
    • 預設狀態下,Bootstrap 會將這兩個區塊隱藏
    • 未來在 JS 中,當我們針對 Input 加上 is-validis-invalid 類別時,對應的提示字才會彈現出來。
💡 進階細節:為什麼「品名」的提示區塊要額外加上 ID?
仔細看第二區塊,品名的提示多加了 id="pname_valid-feedback"。這是因為「品名」的規則比較複雜,未來我們需要透過 AJAX 去資料庫檢查是否重複。如果有重複,我們必須用 JS 透過這個 ID 去動態替換裡面的文字(例如將原本的「不符合規定」改成精準的「該產品名稱已存在!」)。

2-2. 監聽與即時驗證 (GET)

當使用者填寫完畢並將游標移開 (blur) 時,程式會立刻進行驗證。這不僅能提升使用者體驗,也能確保在資料送到後端存檔前,前端已經過初步過濾。

A. 基礎邏輯與欄位監聽

針對「訂購者名稱」與「數量」,我們執行純前端的條件判斷,並透過 Flags (旗標變數) 記錄該欄位是否過關:

$("#username").blur(function () {
    // 判斷字數是否在 1~8 之間
    if ($(this).val().length > 0 && $(this).val().length < 9) {
        $(this).removeClass("is-invalid").addClass("is-valid");
        flag_username = true;
    } else {
        $(this).removeClass("is-valid").addClass("is-invalid");
        flag_username = false;
    }
});

B. 進階驗證:AJAX 檢查資料重複性

這是此步驟最核心的技術。當使用者輸入完「品名」後,前端會主動發送一個 GET 請求,詢問後端資料庫:「請問這個名字有人用了嗎?」

  • 第一層: 先檢查字數是否符合規範 (1~8 字)。
  • 第二層 (AJAX): 若字數合格,則發送請求至 http://localhost:3000/product?pname=${pname}
  • 判斷回傳結果:
    • data.length > 0:代表資料庫裡已經有這筆品名了。此時必須顯示錯誤,並透過 .text() 動態修改紅字提示內容。
    • data.length == 0:代表這是一個全新的品名,可以使用。
$("#pname").blur(function () {
    let pname = $(this).val();
    if (pname.length > 0 && pname.length < 9) {
        // 發送 GET 請求進行唯一性檢查
        $.ajax({
            type: "GET",
            url: `http://localhost:3000/product?pname=${pname}`,
            dataType: "json",
            success: function (data) {
                if (data.length > 0) {
                    // 品名重複,顯示錯誤訊息
                    $("#pname").removeClass("is-valid").addClass("is-invalid");
                    $("#pname_invalid-feedback").text("該產品名稱已存在,不能使用此名稱!");
                    flag_pname = false;
                } else {
                    // 品名可用,顯示成功訊息
                    $("#pname").removeClass("is-invalid").addClass("is-valid");
                    $("#pname_valid-feedback").text("該產品名稱可以使用!");
                    flag_pname = true;
                }
            },
            error: function () {
                // 若 API 連線失敗,使用 SweetAlert2 彈出警告
                Swal.fire({ title: "產品驗證失敗", icon: "error" });
            }
        });
    } else {
        $(this).removeClass("is-valid").addClass("is-invalid");
        flag_pname = false;
    }
});

核心觀念解析:

  • blur 事件的優勢: 不同於 keyup (每打一個字就檢查一次,會造成伺服器負擔),blur 只在使用者完成輸入並離開欄位時觸發一次,是效能與互動之間的最佳平衡。
  • 動態文字回饋 (Dynamic Feedback): 透過 $("#pname_invalid-feedback").text(...),我們可以在不更動 HTML 結構的情況下,根據後端回傳的具體原因(如:格式錯誤、名稱重複、伺服器斷線)來精準提示使用者。
  • 連線錯誤處理: 在 AJAX 中加入 error 區塊是非常重要的習慣。當後端 JSON Server 或 API 伺服器未啟動時,能及時透過 SweetAlert2 給予開發者或使用者明確的錯誤提醒。

2-3. 打包資料與發送 POST 請求 (Create)

當使用者填寫完畢並點擊「確認」按鈕時,我們會觸發 click 事件。此時必須經過最終確認,將資料打包成 JSON 格式,並以 POST 方法送到後端 API 寫入資料庫。

A. 最終防呆與資料封裝

在發送請求前,必須確保所有的驗證旗標 (Flags) 皆為 true。接著,將各個 Input 欄位的值抓出來,並加入當下的時間戳記,組合成一個 JavaScript 物件。

$("#ok_btn").click(function () {
    // 1. 最終防呆檢查:只有全數通過,才執行新增
    if (flag_username && flag_pname && flag_pnum) {
        
        // 2. 將畫面上的資料封裝成 JS 物件
        let jsonData = {
            username: $("#username").val(),
            pname: $("#pname").val(),
            pnum: $("#pnum").val(),
            size: $("#size").val(),
            createdAt: new Date().toLocaleString() // 自動取得當下時間
        };
        
        // ... 接續 AJAX 發送 ...

B. 發送 AJAX POST 請求

將打包好的資料傳送給後端。請特別注意 POST 請求的必要設定:

        // 3. 傳遞至後端 API
        $.ajax({
            type: "POST", // HTTP 請求方法:POST 用於「新增」
            url: "http://localhost:3000/product",
            contentType: "application/json", // 宣告傳送格式為 JSON
            data: JSON.stringify(jsonData),  // 將 JS 物件轉為 JSON 字串
            
            success: function (data) {
                // 新增成功:呼叫 SweetAlert2 顯示成功動畫
                Swal.fire({
                    title: "新增成功!",
                    icon: "success",
                    confirmButtonText: "確認"
                }).then((result) => {
                    // 使用者點擊「確認」後,清空輸入欄位以利下一筆輸入
                    if (result.isConfirmed) {
                        $("#username").val("").blur();
                        $("#pname").val("").blur();
                        $("#pnum").val("1").blur();
                        $("#size").val("大");
                    }
                });
            },
            error: function () {
                // API 連線失敗或伺服器出錯
                Swal.fire({ title: "新增失敗", icon: "error" });
            }
        });

    } else {
        // 若有任何 flag 為 false,則擋下並提示使用者
        Swal.fire({ title: "欄位有錯誤,請修正!", icon: "error" });
    }
});

核心觀念解析:

  • JSON.stringify()contentType 在執行 POST 或 PATCH 等寫入動作時,這兩者缺一不可。必須明確告訴伺服器「我傳的是 JSON (contentType)」,並且確實「把物件轉成純文字字串傳輸 (stringify)」。
  • 優化 UX 的小技巧 (清空表單): 在 SweetAlert2 的 .then() 區塊中,當成功寫入資料並獲得使用者確認後,我們使用 .val("").blur() 將欄位清空。特別注意 .blur() 的用意: 因為原本的欄位帶有綠色勾勾 (is-valid),透過強制觸發 blur,可以讓前面寫好的驗證機制重新啟動,把綠框洗掉,恢復成乾淨的原始狀態。
💡 SweetAlert2 (Swal) 的 Promise 特性:
不同於傳統的 alert() 會直接卡死網頁執行緒,SweetAlert2 是一個非同步的彈出視窗。如果你希望「等使用者按下確認按鈕後,才執行某件事情」(例如跳轉頁面或清空表單),必須將後續的程式碼寫在 .then((result) => { ... }) 區塊之內。

3. 讀取與渲染列表 (Read)

完成新增功能後,我們需要一個專屬的頁面來顯示資料庫中所有的產品。這個頁面包含一個資料表格,以及一個隱藏的「更新用彈出視窗 (Modal)」。

3-1. 準備 showTable 檔案、刻入架構

20260310-CRUD-showTable.html 中,我們不寫死表格的內容,而是留下 id 讓 AJAX 抓到資料後動態填入。同時,我們將 Bootstrap 的 Modal 先寫在網頁底部備用。

<!-- 1. 資料列表區塊 -->
<div class="container pt-5">
    <div class="display-4 text-primary fw-900 mb-3 text-center">顯示產品列表</div>
    <table class="table table-bordered shadow-sm">
        <thead class="table-dark">
            <tr>
                <th>編號</th>
                <th>訂購者</th>
                <th>品名</th>
                <th>數量</th>
                <th>尺寸</th>
                <th>建檔時間</th>
                <th>#</th> <!-- 預留給操作按鈕 (更新/刪除) -->
            </tr>
        </thead>
        <!-- 這是最關鍵的 Hook,未來 AJAX 取得的資料都會 append 到這裡 -->
        <tbody id="mybody">
            <!-- 資料將由 JS 動態生成 -->
        </tbody>
    </table>
</div>

<!-- 2. 更新專用的彈出視窗 (Modal) -->
<div class="modal fade" id="updateModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header text-bg-warning">
                <h1 class="modal-title fs-5 fw-900">產品更新</h1>
                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
            </div>
            <div class="modal-body">
                <!-- 訂購者與品名:設定為 disabled (禁止修改) -->
                <div class="mb-3">
                    <label class="form-label">訂購者名稱</label>
                    <input type="text" class="form-control" id="username" disabled>
                </div>
                <div class="mb-3">
                    <label class="form-label">品名</label>
                    <input type="text" class="form-control" id="pname" disabled>
                </div>
                
                <!-- 數量與尺寸:開放修改 -->
                <div class="mb-3">
                    <label class="form-label">數量(1~10)</label>
                    <input type="number" class="form-control" id="pnum">
                </div>
                <div class="mb-3">
                    <label class="form-label">尺寸</label>
                    <select id="size" class="form-select form-select-lg">
                        <option value="大">大</option>
                        <option value="中">中</option>
                        <option value="小">小</option>
                    </select>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
                <button type="button" class="btn btn-primary" id="update_btn">確認更新</button>
            </div>
        </div>
    </div>
</div>

核心設計思維解析:

  • 動態資料的容器: <tbody id="mybody"> 是整個頁面的靈魂。因為我們不知道資料庫裡有幾筆資料,所以網頁初始狀態必須是空的,由 JavaScript 抓到資料後再來「填空」。
  • Modal 視窗的預先掛載: Bootstrap 的 Modal 是透過 CSS 控制顯示與隱藏(預設隱藏)。我們把它寫在網頁底部,未來當使用者點擊表格中的「更新」按鈕時,只要透過屬性或 JS 呼叫 #updateModal 即可喚醒它。
  • 部分欄位鎖定 (disabled): 在商業邏輯中,通常不允許修改核心欄位(如訂購者是誰、買了什麼商品),只能修改細節(如數量、尺寸)。因此,我們在 Modal 中針對 usernamepname 加上了 disabled 屬性,這在畫面上會呈現灰色且無法點擊的狀態,是很常見的 UI 防呆設計。

3-2. 取得資料與狀態初始化 (GET)

showTable 頁面載入完成後,我們必須主動向後端請求資料,並將回傳的結果顯示在網頁表格中。這需要結合 AJAX GET 請求與「全域變數」的儲存機制。

A. 變數宣告與環境初始化

在主程式執行前,我們先定義幾個全域變數來儲存狀態與資料:

let productDATA = []; // 關鍵!用來儲存從後端抓回來的所有產品 JSON 資料
let uid;              // 預留變數,用於後續「更新」功能時暫存該筆資料的唯一的 ID
let flag_pnum = true; // 驗證旗標,用於更新視窗中的數量驗證

B. 發送 AJAX GET 請求

透過 jQuery 的 $.ajax 方法,我們向指定的 API 網址請求所有產品資料:

$(function () {
    // 當網頁載入完成,立刻執行取得資料的動作
    $.ajax({
        type: "GET",                 // 使用 GET 方法讀取資料
        url: "http://localhost:3000/product",
        dataType: "json",            // 指定回傳格式為 JSON
        success: function (data) {
            // 請求成功:
            console.log(data);       // 在主控台確認資料內容
            productDATA = data;      // 將抓回來的陣列存入全域變數,供全網頁程式使用
            
            // 呼叫渲染函數,將資料跑迴圈畫到 HTML 表格上
            renderTable(productDATA);
        },
        error: function () {
            // 請求失敗:代表伺服器未啟動或網址打錯
            Swal.fire({
                title: "讀取失敗",
                text: "API 連結出錯:http://localhost:3000/product",
                icon: "error"
            });
        }
    });
});

核心觀念解析:

  • 全域變數 productDATA 的妙用: 我們特意將 API 回傳的資料儲存在全域變數中,而非直接畫完就丟。這樣做的好處是,未來如果要執行「前端即時關鍵字搜尋」或「欄位升降排序」時,就不需要再次驚動伺服器,直接操作這個變數即可,網頁反應會快非常多。
  • 資料驅動介面 (Data-Driven): 這是一種現代開發思維。我們不直接操作 DOM 來改文字,而是「先處理資料 (JS 變數)」,再由專門的函數(如 renderTable)根據這份資料來產出畫面。
  • 錯誤處理的必要性: AJAX 是非同步的,若後端掛掉,前端網頁通常會沒反應。因此,必須加上 error 區塊並搭配 SweetAlert2 提醒,開發者才能第一時間知道是連線出了問題。
💡 資料流向示意圖:
資料庫 (db.json) ➔ API Server (json-server) ➔ 前端 AJAX GET ➔ productDATA 變數 ➔ renderTable() 函數 ➔ 網頁顯示。

3-3. 動態渲染表格與資料埋設 (Render Table)

當 AJAX 成功抓回 productDATA 後,我們需要將這些記憶體中的數據逐一「畫」到網頁表格上。這需要透過 JavaScript 的迴圈機制來處理。

A. 遍歷資料與拼接 HTML

利用 forEach 迴圈遍歷陣列,並使用 ES6 樣板字面值 (反引號 ` ) 輕鬆處理多行字串,將物件屬性嵌入到 <tr><td> 標籤中。

function renderTable(data) {
    // 遍歷從後端取得的每一筆產品物件 (item)
    data.forEach(function (item) {
        // 利用反引號建構整行表格列
        let strHTML = `<tr>
            <td>${item.id}</td>
            <td>${item.username}</td>
            <td>${item.pname}</td>
            <td>${item.pnum}</td>
            <td>${item.size}</td>
            <td>${item.createdAt}</td>
            <td>
                <!-- 更新按鈕:埋入大量 data-* 屬性,為後續 U 流程鋪路 -->
                <button class="btn btn-outline-warning updateBtn" 
                        data-bs-toggle="modal" 
                        data-bs-target="#updateModal" 
                        data-id="${item.id}" 
                        data-username="${item.username}"
                        data-pname="${item.pname}"
                        data-pnum="${item.pnum}"
                        data-size="${item.size}">更新</button>
                
                <!-- 刪除按鈕:只需埋入 id 供刪除辨識 -->
                <button class="btn btn-danger deleteBtn" data-id="${item.id}">刪除</button>
            </td>
        </tr>`;
        
        // 將拼接好的 HTML 字串插入到表格主體 (#mybody) 之中
        $("#mybody").append(strHTML);
    });
}

B. 核心技巧:data-* 屬性的深度運用

這段程式碼最精妙的地方在於按鈕上的 data-* 自訂屬性。這等於是在每個按鈕上「貼標籤」。

  • 為什麼要埋資料? 由於表格是動態生成的,當使用者點擊「更新」按鈕時,程式必須立刻得知「這筆資料原本是什麼內容」。
  • 資料流向: 我們將資料庫裡的 ID、姓名、品名等資訊,利用 data-id="${item.id}" 等語法直接塞進按鈕的 HTML 屬性中。
  • 取用方式: 在後續的「更新 (Update)」步驟,我們只需要使用 jQuery 的 $(this).data("id")$(this).data("pname"),就能輕易把資料從按鈕中取回,填入 Modal 彈出視窗。
💡 小提醒:
在您的程式碼中,按鈕標籤寫為 <butoon>,實務開發時請記得更正為標準的 <button>,以確保瀏覽器能正確渲染樣式並獲得最佳的相容性。

4. 更新資料 (Update)

更新功能是 CRUD 中邏輯最複雜的一環,因為它需要先「讀取舊資料」,再「寫入新資料」。這通常會配合一個隱藏的表單或 Modal (彈出視窗) 來進行。

4-1. 事件委派與資料回填至 Modal

當使用者點擊表格中的「更新」按鈕時,我們必須把這筆資料的詳細內容填入 Modal 中。但這裡有一個極大的陷阱:表格是 AJAX 非同步「事後」畫上去的,傳統的 click() 監聽會失效!

// 錯誤寫法:$(".updateBtn").click(function(){...}) 會抓不到動態生成的按鈕!

// 正確寫法:事件委派 (Event Delegation)
// 將監聽器綁定在早就存在的 document 上,當點擊發生時,再去過濾是否為 .updateBtn
$(document).on("click", ".updateBtn", function () {
    // 1. 抓取埋在按鈕上的 data-* 屬性
    console.log($(this).data("id"));
    console.log($(this).data("pname"));

    // 2. 將重要的唯一識別碼 (ID) 存入全域變數 uid 中,稍後更新 API 會用到
    uid = $(this).data("id");

    // 3. 將抓到的舊資料,回填 (val) 到 Modal 的輸入框內
    $("#username").val($(this).data("username"));
    $("#pname").val($(this).data("pname"));
    $("#pnum").val($(this).data("pnum"));
    $("#size").val($(this).data("size"));
});

4-2. 發送 PATCH 更新請求

當使用者在 Modal 中修改完「數量」與「尺寸」,點擊「確認更新」按鈕時,我們就要把新資料送給後端。請注意 HTTP Method 的使用方式:

💡 HTTP 更新方法的差異:
  • PUT: 替換整筆資料。如果只傳了數量,其他的名字、尺寸等欄位可能會被後端清空。
  • PATCH: 部分更新。只會修改你有傳遞過去的欄位,其他欄位保持原樣。(現代開發推薦使用)
// 監聽 Modal 裡面的更新按鈕
$("#update_btn").click(function () {
    // 1. 執行更新前的欄位驗證 (確保數量輸入正確)
    if (flag_pnum) {
        // 2. 取得使用者修改後的新數值
        let pnum = $("#pnum").val();
        let size = $("#size").val();

        // 3. 發送 AJAX 請求至後端 API
        $.ajax({
            type: "PATCH", // 使用 PATCH 進行部分資料更新
            url: `http://localhost:3000/product/${uid}`, // 網址必須加上該筆資料的 ID (${uid})
            contentType: "application/json",
            
            // 4. 將要更新的欄位打包成 JSON 字串送出
            // (因為 username 跟 pname 被設定為 disabled 不允許修改,所以不送)
            data: JSON.stringify({ pnum: pnum, size: size }),
            
            success: function (data) {
                // 更新成功後的處理
                Swal.fire({
                    title: "更新成功!",
                    icon: "success",
                    confirmButtonText: "確認"
                }).then((result) => {
                    if (result.isConfirmed) {
                        // 強制重新載入網頁,讓表格顯示最新資料
                        location.href = "20260310-CRUD-showTable.html";
                    }
                });
            },
            error: function () {
                Swal.fire({ title: "更新失敗", icon: "error" });
            }
        });
    } else {
        // 欄位驗證未通過的警告
        Swal.fire({ title: "欄位有錯誤!", text: "數量必須為 1~10", icon: "error" });
    }
});

核心觀念解析:

  • $(document).on(...) 的必要性: 這是 jQuery 處理動態生成元素(Dynamically generated elements)的黃金法則。它利用了瀏覽器「事件冒泡(Event Bubbling)」的原理,把監聽器架設在最外層的 document,來攔截內部所有按鈕的點擊。
  • 全域變數 uid 在點擊「表格更新按鈕」到點擊「Modal 儲存按鈕」之間,程式經歷了兩次不同的事件。透過把 ID 存入全域變數 uid,我們才能在最後一刻準確知道:剛剛使用者到底是在編輯哪一筆商品?
  • 精準打擊的 URL 路由: url: `.../product/${uid}`,在 RESTful API 設計中,針對特定資料進行修改(PATCH/PUT)或刪除(DELETE)時,都必須在 URL 的結尾精準帶上該筆資料的 ID,後端才知道要操作資料庫裡的哪一列。

5. 刪除資料 (Delete)

刪除是資料操作中不可逆的行為。為了確保安全性與使用者體驗,我們必須實作「二次確認機制」,並正確地利用 RESTful API 的 DELETE 方法來通知伺服器移除資料。

5-1. 實作邏輯與流程拆解

刪除功能的運作分為:監聽點擊、使用者確認、發送請求、以及畫面同步四個步驟。

// 1. 監聽刪除按鈕 (同樣使用事件委派,確保能抓到動態生成的按鈕)
$(document).on("click", ".deleteBtn", function () {
    
    // 2. 呼叫 SweetAlert2 彈出詢問視窗
    Swal.fire({
        title: "確認刪除?",
        showDenyButton: true,      // 顯示取消按鈕
        icon: "question",          // 顯示問號圖示
        confirmButtonText: "確認",
        denyButtonText: `取消`
    }).then((result) => {
        
        // 3. 使用者按下「確認」後才執行刪除邏輯
        if (result.isConfirmed) {
            // 抓取埋在按鈕上的資料 ID
            let delete_id = $(this).data("id");

            // 4. 發送 AJAX DELETE 請求
            $.ajax({
                type: "DELETE", // HTTP 方法:DELETE 用於移除資源
                url: `http://localhost:3000/product/${delete_id}`, // 指定該筆 ID 的路徑
                dataType: "json",
                success: function (data) {
                    // 5. 刪除成功後,強制跳轉/重新載入頁面以同步資料
                    location.href = "20260310-CRUD-showTable.html"; 
                },
                error: function () {
                    Swal.fire({ title: "刪除失敗", icon: "error" });
                }
            });
        }
    });
});

核心觀念解析:

  • DELETE 請求方法: 依照 RESTful 規範,刪除資料應使用 DELETE 方法。通常這類請求不需要傳送 data 本體(Payload),伺服器只需根據 URL 中的 ID 就能判斷要刪除哪一筆紀錄。
  • 使用者體驗 (UX) 與安全性: 直接執行刪除是非常危險的。透過 SweetAlert2 的 .then() 判斷使用者是否真的點擊了 isConfirmed,能有效防止因滑鼠誤觸而導致的資料流失。
  • 資料同步策略: 在此範例中,成功刪除後使用了 location.href 重新載入頁面。
    💡 優化思維:
    雖然重整網頁最保險,但會造成畫面閃爍。在更進階的開發中,我們可以選擇「不重整網頁」,直接透過 jQuery 的 $(this).closest('tr').remove() 將該行表格從 DOM 中移除,達成更絲滑的視覺效果。
🎯 CRUD 實戰總結:
至此,我們已經完成了完整的資料生命週期管理。從 C (POST) 新增、R (GET) 讀取渲染、U (PATCH) 部分更新,到最後的 D (DELETE) 刪除,所有的動作都圍繞著「非同步 AJAX」與「資料屬性埋設」這兩大核心技術展開。